package com.meadhall.sync2cloud;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JLabel;
import javax.swing.SwingWorker;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSelectInfo;
import org.apache.commons.vfs.FileSelector;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.VFS;

import com.rackspacecloud.client.cloudfiles.FilesClient;
import com.rackspacecloud.client.cloudfiles.FilesObject;

/**
 * Performs remote-to-local synchronization. Extends SwingWorker to work as a background task in Swing application.
 * 
 * @author Marshall Levin
 */
public class R2LSync extends SwingWorker<Void, String> {

	private static final String DIRECTORY_MIME_TYPE = "application/directory";

	private File localRoot;
	private String remoteContainer;
	private FilesClient client;
	private JLabel statusLabel;

	/**
	 * Checks parameters and initializes the service.
	 * 
	 * @param localRoot
	 *            specifying the local path
	 * @param remoteContainer
	 *            specifying the remote container
	 * @param client
	 *            a Rackspace CloudFiles client
	 * @param statusLabel
	 *            for displaying status information
	 * @throws FileSyncException
	 */
	public R2LSync(File localRoot, String remoteContainer, FilesClient client, JLabel statusLabel) throws FileSyncException {
		publish("Checking parameters");
		if (localRoot == null || !localRoot.exists() || !localRoot.isDirectory() || !localRoot.canRead()) {
			throw new FileSyncException("Local root directory is not valid");
		}
		if (remoteContainer == null || remoteContainer.trim().equals("")) {
			throw new FileSyncException("Remote container name is not valid");
		}
		if (client == null || !client.isLoggedin()) {
			throw new FileSyncException("Client must be authenticated");
		}
		if (statusLabel == null) {
			throw new FileSyncException("Label element cannot be null");
		}
		this.localRoot = localRoot;
		this.remoteContainer = remoteContainer;
		this.client = client;
		this.statusLabel = statusLabel;
		publish("System initialized");
	}

	/**
	 * Kicks off the initial synchronization.
	 * 
	 * @throws FileSyncException
	 */
	public void initialSync() throws FileSyncException {
		publish("Performing initial sync");
		checkContainer();
		copyRemoteToLocal();
	}

	/**
	 * Copies remote files to local location. Builds a list of files in each location and compares them, copying or deleting as necessary to make the local location mirror the remote one.
	 * 
	 * @throws FileSyncException
	 */
	private void copyRemoteToLocal() throws FileSyncException {

		try {

			// Build a map of local and remote files

			publish("Building map of local files");
			FileObject[] localFiles = VFS.getManager().toFileObject(localRoot).findFiles(new FileSelector() {
				public boolean includeFile(FileSelectInfo fileInfo) {
					File tmpFile;
					try {
						tmpFile = new File(fileInfo.getFile().getURL().getFile());
					} catch (FileSystemException fse) {
						return false;
					}
					return !tmpFile.equals(localRoot);
				}

				public boolean traverseDescendents(FileSelectInfo fileInfo) {
					return true;
				}
			});
			Map<String, File> localFileMap = new HashMap<String, File>();
			for (int i = 0; i < localFiles.length; i++) {
				File tmpFile = new File(localFiles[i].getURL().getFile());
				localFileMap.put(getRemoteStylePath(tmpFile), tmpFile);
			}
			publish("Building map of remote files");
			Collection<FilesObject> remoteFiles = client.listObjects(remoteContainer);
			Map<String, FilesObject> remoteFileMap = new HashMap<String, FilesObject>();
			for (FilesObject file : remoteFiles) {
				remoteFileMap.put(file.getName(), file);
			}

			// For each remote file, copy to local file if different or not present
			publish("Finding files to be copied");
			for (String filename : remoteFileMap.keySet()) {
				FilesObject file = remoteFileMap.get(filename);
				File localFile = new File(localRoot, filename);
				if (!localFileMap.containsKey(filename)) {
					// local file does not exist
					publish("Copying " + filename + " from server");
					if (file.getMimeType().equals(DIRECTORY_MIME_TYPE)) {
						// local file is not present and should be a directory, so create it (and any necessary parent directories as well)
						FileUtils.forceMkdir(localFile);
					} else {
						// create parent directory
						FileUtils.forceMkdir(localFile.getParentFile());
						// local file is not present and should be a normal file, so simply download it
						if (file.getSize() == 0) {
							FileUtils.touch(localFile);
						} else {
							InputStream fileData = client.getObjectAsStream(remoteContainer, filename);
							FileOutputStream outputStream = FileUtils.openOutputStream(localFile);
							IOUtils.copyLarge(fileData, outputStream);
							outputStream.close();
							fileData.close();
						}
					}
				} else {
					// local file exists
					if (file.getMimeType().equals(DIRECTORY_MIME_TYPE)) {
						// make sure the local file is also a directory
						if (!localFile.isDirectory()) {
							FileUtils.forceDelete(localFile);
							FileUtils.forceMkdir(localFile);
						}
					} else {
						// copy only if different
						String localHash = FilesClient.md5Sum(localFile);
						String remoteHash = remoteFileMap.get(filename).getMd5sum();
						if (!localHash.equalsIgnoreCase(remoteHash)) {
							// create parent directory
							FileUtils.forceMkdir(localFile.getParentFile());
							// copy file
							if (file.getSize() == 0) {
								FileUtils.touch(localFile);
							} else {
								InputStream fileData = client.getObjectAsStream(remoteContainer, filename);
								FileOutputStream outputStream = FileUtils.openOutputStream(localFile);
								IOUtils.copyLarge(fileData, outputStream);
								outputStream.close();
								fileData.close();
							}
						}
					}
				}
			}

			// For each local file, if not present remotely, remove it
			for (String filename : localFileMap.keySet()) {
				if (!remoteFileMap.containsKey(filename)) {
					publish("Removing local file " + filename);
					File localFile = new File(localRoot, filename);
					FileUtils.forceDelete(localFile);
				}
			}

		} catch (Exception e) {
			throw new FileSyncException("Unable to perform sync", e);
		}

	}

	/**
	 * Confirms that the remote container exists.
	 * 
	 * @throws FileSyncException
	 */
	private void checkContainer() throws FileSyncException {
		publish("Checking container");
		try {
			boolean exists = client.containerExists(remoteContainer);
			if (!exists) {
				throw new FileSyncException("Container does not exist");
			}
		} catch (Exception e) {
			throw new FileSyncException("Unable to check remote container", e);
		}
	}

	/**
	 * Convert a Windows-style path to a UNIX-style path appropriate for CloudFiles.
	 * 
	 * @param file
	 *            specifying the local file object
	 * @return a String representing the path relative to the container
	 */
	private String getRemoteStylePath(File file) {
		String path = null;
		if (file != null) {
			path = file.getAbsolutePath();
			path = FilenameUtils.getPath(path) + FilenameUtils.getName(path);
			path = FilenameUtils.separatorsToUnix(path);
			path = path.substring(localRoot.getName().length() + 1); // omit name of root directory
		}
		return path;
	}

	/**
	 * SwingWorker method to allow task to be formed in the background. Watches isCancelled() to allow the task to be stopped. Performs the initial synchronization and then repeats it every 5 seconds.
	 */
	@Override
	protected Void doInBackground() throws Exception {
		initialSync();
		while (!isCancelled() && !Thread.interrupted()) {
			try {
				copyRemoteToLocal();
			} catch (FileSyncException fse) {
				throw new RuntimeException(fse);
			}
			try {
				Thread.sleep(5 * 1000);
			} catch (InterruptedException ie) {
			}

		}
		return null;
	}

	/**
	 * Updates the UI with the status of the system.
	 */
	@Override
	protected void process(List<String> updates) {
		super.process(updates);
		statusLabel.setText(updates.get(updates.size() - 1));
	}

}